C++ Các chức năng hướng đối tượng C++

C++ dẫn nhập thêm một số chức năng hướng đối tượng (OO) lên C. Nó cung cấp các lớp mà có 4 chức năng thông dụng trong các ngôn ngữ OO: tính trừu tượng, tính đóng gói, tính đa hình, và tính kế thừa.

Lưu ý: trong phần này các từ "hàm nội tại", "phương pháp", hay "hàm" đều có cùng một nghĩa là "phương thức thuộc về một lớp".

Tính đóng gói

C++ xây dựng tính đóng bằng cách cho phép mọi thành viên của một lớp có thể được khai báo bằng các từ khoá public, private, hay protected. (xem thêm các khái niệm cơ bản trong ngôn ngữ OOP). Một thành viên private chỉ có thể được truy cập từ các phương pháp (hàm nội tại) là thành viên của chính lớp đó hay được truy cập từ các hàm và các lớp được đặc biệt cho phép sử dụng bằng cách dùng từ khóa friend. Một thành viên protected của một lớp sẽ có thể truy cập được từ các thành viên (nào đó) của các lớp có tính kế thừa của nó hay cũng có thể truy cập được từ các thành viện của chính lớp đó và của mọi thành viên friend.

Nguyên lý của OOP là mọi và chỉ có các hàm là có thể truy cập được đến các giá trị nội tại của cùng lớp thì nên có tính đóng. C++ có hỗ trợ đặc tính này (qua các hàm thành viên và các hàm friend), nhưng C++ lại không là yêu cầu bắt buộc: người lập trình có thể khai báo các phần hay tất cả các giá trị nội tại là công cộng (public), và cũng cho phép làm cho toàn bộ lớp trở thành công cộng. Lý do là vì C++ hỗ trợ không chỉ lập trình hướng đối tượng mà còn hỗ trợ các mẫu hình yếu hơn như là lập trình mô-đun.

Một thói quen tốt cần có trong thực hành là khai báo mọi dữ liệu đều là riêng tư (private), hay ít nhất ở dạng bảo tồn, và sau đó, tạo ra một giao diện nhỏ (thông qua các phương pháp) cho người dùng của lớp này dấu đi các chi tiết thiết lập bên trong.

Tính đa hình

Khái niệm đa hình được dùng khá rộng rãi và là khái niệm bị lạm dụng cũng như không được định nghĩa rõ ràng.

Nói chung tính đa hình trong lập trình hướng muốn nói đến 1 đoạn code nhưng trong 2 trường hợp khác nhau có thể xuất ra 2 kết quả khác nhau. Vì tính chất ra nhiều kết quả khác nhau này nên nó được gọi là đa hình.

Trong trường hợp của C++, khái niệm này thường được nối kết với các tên của các hàm thành viên. Các hàm thành viên này có cùng tên, sự khác nhau chỉ có thể được dựa vào một hay cả hai yếu tố sau:

  1. Số lượng và kiểu của các đối số (tức là nguyên mẫu của hàm) -- Tính chất này gọi là đa hình tĩnh (static polymorphism)
  2. Kiểu lớp mà thực thể thực sự thuộc vào. Tính chất này được dùng khi hàm thành viên được định nghĩa là hàm ảo qua từ khóa virtual—tính chất này gọi là đa hình động (dynamic polymorphism)

Khi được gọi thì chương trình sẽ tùy theo hai yếu tố trên để xác định chính xác hàm nào phải được thực thi trong số các hàm cùng tên.

Ví dụ sau đây mô tả tính đa hình:

 1     /* Static polymorphism */ 2  3     extern int SendJobToDevice(PrintJobText *, DeviceLaser *); 4     extern void SendJobToDevice(PrintJobText *, DeviceJet *); 5     extern void SendJobToDevice(PrintJobHTML *, DeviceLaser *); 6     extern void SendJobToDevice(PrintJobHTML *, DeviceJet *);... 7     SendJobToDevice(printJob, device); 8  9     /* Dynamic polymorphism */10 11     class Device {12     public:13       virtual void print(PrintJob*);...14     };15 16     PrintJob *printJob;17     Device *device;...18     device->print(printJob);19     // Note that since C++ does not have multiple dispatch, the above20     // function call is polymorphic based only on the device's type.

Ví dụ thứ hai về tính đa hình động:

 1     class Nguoi 2     { 3     public: 4       virtual void Chao() // Hàm ảo 5       { 6         cout << "Toi chua biet chao";	 7       }; 8     }; 9     //------------------10     class NguoiViet: public Nguoi11     {12     public:13       void Chao()14       {15         cout << "Xin chao.";16       }17     };18     //------------------19     class NguoiAnh: public Nguoi20     {21     public:22       void Chao()23       {24         cout << "Hello.";25         }26     };27     //------------------28     int main()29     {30       Nguoi *n; NguoiViet nv; NguoiAnh na;31       n = &nv;32       n->Chao(); // (*)33       n = &na;34       n->Chao(); // cùng dòng code voi (*) nhưng lại cho kết quả khác35       return 0;36     }

Trong C, thì đa hình (động) có thể đạt tới bằng cách dùng từ khóa switch hay dùng con trỏ hàm.

C++ còn cung cấp hai tính năng độc đáo cho đa hình là:

  • Nạp chồng toán tử (overloading): Cho phép một toán tử hay một hàm có những ứng xử khác nhau phụ thuộc vào kiểu của các toán hạng hay các tham số tại thời điểm toán tử hay hàm được triệu gọi.

Ví dụ, ta có thể định nghĩa hai hàm trùng tên như sau:float Demo(float a, float b) {return a + b;} và int Demo(int a, int b) {return a - b;}Ta cũng có thể tải bội phép cộng cho lớp MATRIX để có thể viết được C = A + B khi A, B và C có kiểu MATRIX.

  • Tính ảo (virtual): Cho phép một phương thức (hàm thành viên hoặc toán tử) của lớp có ứng xử khác nhau phụ thuộc vào sự kế thừa của lớp con cháu (Xem phương thức Chao() trong ví dụ trên.

Hai tính năng trên cho phép chương trình định ra nhiều sự thiết lập khác nhau của một hàm để sử dụng ứng với các kiểu (khác nhau) của các đối tượng.

Việc quá tải hàm cho phép các chương trình khai báo nhiều hàm có chung một tên (ngay cả việc các hàm này thuộc cùng một lớp). Các hàm này phân biệt được bởi số lượng và kiểu của các tham số. Ví dụ, một chương trình có thể có khai báo 3 hàm sau:

1     void pageUser(int userid);2     void pageUser(int userid, string message);3     void pageUser(string username);

Sau, đó, khi trình dịch đọc phải câu lệnh có gọi tới hàm pageUser(), thì mó sẽ xác định xem đó là hàm nào tùy dựa trên số lượng và kiểu của các đối số đã được đưa vào (tức là dựa vào sự khác nhau của các nguyên mẫu của những hàm này). Lý do ta gọi kiểu quá tải hàm này là đa hình tĩnh vì nó được phân lập trong thời gian dịch mã.

Chú ý: trình dịch sẽ không phân biệt khác nhau về kiểu trả về, do đó không thể quá tải hai hàm hoàn toàn giống nhau trong cùng một lớp mà lại chỉ khác nhau về kiểu trả về.

Quá tải toán tử (operation overloading) là một dạng của quá tải hàm. Nó là một trong những đặc tích của C++ bị nhiều tranh cãi nhất. Nhiều người cho rằng việc quá tải toán tử đã bị lạm dụng quá đáng, nhưng nhiều người khác nghĩ rằng đây là công cụ rất mạnh để tăng cường sự biểu thị (qua ký các hiệu toán tử).

Toán tử là một trong những ký hiệu đã được định nghĩa trong ngôn ngữ C++ đóng vai trò của các toán tử để thực hiện các phép toán trên các kiểu dữ liệu. Quá tải toán tử được hiểu là quá trình hay phương thức để tái sử dụng một toán tử sẵn có để định nghĩa và dùng cho một phép toán khác.

Danh sách các toán tử có thể thực hiện quá tải

+ - * / = < > += -= *= /= << 
>> <<= >>= ==!= <= >= ++ -- % & ^
! | ~ &= ^= |= && || %= [] () new delete

Việc quá tải hàm cho phép người lập trình định nghĩa nhiều phiên bản khác nhau của một hàm để dùng với các kiểu đối số khác nhau trong khi việc quá tải toán tử lại cho phép người lập trình định nghĩa nhiều phiên bản khác nhau của một toán tử để dùng với các kiểu phép toán khác nhau.

    Integer& operator++();

thì chương trình này có thể dùng toán tử ++ với các đối tượng của kiểu Integer. Ví dụ như:

1     Integer a = 2;2     ++a;

sẽ ứng xử tương đương với:

1     Integer a = 2;2     a.operator++();

Trong phần lớn trường hợp, đoạn mã nguồn trên sẽ làm tăng giá trị của biến a lên 3. Tuy nhiên, lập trình viên viết lớp Integer có thể định nghĩa toán tử Integer::operator++() làm bất cứ gì lập trình viên muốn. Vì toán tử thường được dùng ngầm, lập trình viên không nên khai báo toán tử trừ trường hợp ý nghĩa của toán tử là rõ ràng và không gây nhầm lẫn. Tuy nhiên, có nhiều ý kiến cho rằng thư viện chuẩn C++ không tuân theo quy ước này. Ví dụ, đối tượng cout, được sử dụng để xuất ký tự ra màn hình có toán tử quá tải <<, nhiều người cho rằng toán tử << là không rõ ràng và vô nghĩa trong trường hợp muốn xuất ký tự ra màn hình do toán tử này cũng là toán tử được dùng trong phép tính dịch bit. Tuy nhiên, phần lớn lập trình viên cho rằng cách sử dụng toán tử << trong trường hợp cout là có thể chấp nhận được.

Tiêu bản C++ sử dụng rất nhiều tính đa hình tĩnh, trong đó bao gồm cả các toán tử được quá tải.

Hàm ảo cung cấp một kiểu đa hình khác. Trong trường hợp này, các đối tượng có cùng một lớp cơ sở có thể sử dụng một hàm một cách khác nhau. Ví dụ, lớp cơ sở PrintJob bao gồm hàm thành viên:

     virtual int getPageCount(double pageWidth, double pageHeight)

Mỗi cách khác nhau của công việc in như là DoubleSpacedPrintJob, có thể trở thành phương pháp ưu tiên với một hàm mà có thể tính được gần đúng số trang của công việc in theo cách đó. Ngược lại, với việc quá tải hàm, các tham số của một hàm thành phần cho trước thì luôn luôn xác định và không đổi về số lượng và kiểu. Chỉ có kiểu của đối tượng (mà theo đó tên của phương pháp này được gọi) là có thay đổi.

Khi một hàm thành viên ảo của một đối tượng được gọi thì trình dịch đôi khi không được kiểu của đối tượng này ở thời gian dịch và do đó không thể xác định hàm (quá tải) nào để gọi. Quyết định gọi này bởi vậy phải để vào thời gian thực thi. Trình dịch sẽ tạo ra các mã để kiểm tra lại kiểu của đối tượng ở thời gian thi hành và từ đó xác định hàm nào để gọi. Bởi vì việc xác định hàm chỉ xảy ra lúc chạy chương trình nên phương pháp quá tải hàm này được gọi là đa hình động.

Sự xác định và thi hành của một hàm trong thời gian thực thi gọi là điều phối động. Trong C++, việc này thường hoàn tất bằng cách dùng các bảng ảo.

Tính kế thừa

Kế thừa từ một lớp cơ sở có thể được khai báo thông qua các đặc tính công cộng, bảo tồn, hay riêng tư. Những đặc tính này cho phép xác định khi nào các lớp dẫn xuất hay không liên quan có thể sử dụng các thành viên công cộng, bảo tồn, hay riêng tư của lớp cơ sở. Tuy nhiên, chỉ có sự kế thừa dạng công cộng là hoàn toàn theo đúng ý nghĩa của việc "kế thừa". Hai dạng khác thì ít được dùng hơn. Nếu các đặc tả này không được khai báo thì việc kế thừa được gán mặc định là dạng riêng tư cho lớp cơ sở và dạng công cộng cho một cấu trúc cơ sở.

Các lớp cơ sở có thể được khai báo là ảo (thông qua từ khóa virtual). Kế thừa ảo bảo đảm rằng chỉ có một thực thể của lớp cơ sở tồn tại trong đồ thị kế thừa, tránh được một số vấn đề mơ hồ của việc đa kế thừa.

Đa kế thừa cũng là một tính năng có nhiều tranh cãi trong C++. Tính đa kế thừa cho phép một lớp được dẫn xuất từ nhiều hơn một lớp cơ sở; điều này có thể dẫn tới một đồ thị phức tạp của các quan hệ kế thừa. Ví dụ, lớp "Buổi học" có thể kế thừa từ hai lớp "thời gian" và "bộ môn". Một số ngôn ngữ khác như Java, tiến hành cách thức tương tự bằng cách cho phép kế thừa của nhiều giao diện trong khi giới hạn số lượng của các lớp cơ sở (kế thừa) chỉ còn là một lớp. (giao diện, không như lớp, không cho phép thiết lập nội dung của các thành viên và do đó không thể có thực thể)